#include "jicmpsocket_p.h"
#if defined(Q_OS_UNIX) || defined(Q_OS_UNIX)

#include <QSocketNotifier>
#include <QTimerEvent>

#include <unistd.h>
#include <netdb.h>
#include <time.h>

#include <sys/socket.h>
#include <sys/types.h>
#include <sys/time.h>

#include <netinet/ip_icmp.h>
#include <netinet/in.h>
#include <netinet/ip.h>

#include <arpa/inet.h>
#include <net/if.h>

namespace QTE
{

class Q_DECL_HIDDEN PingTask : public QObject
{
public:
	explicit PingTask(int id, JICMPSocketPrivate *d_ptr);
	~PingTask();

public:
	bool start(const QHostAddress &address, int cnt);
	bool start(const QString &domainName, int cnt);
	QString hostAddressString() const;

protected:
	void timerEvent(QTimerEvent *event) override;

private:
	void on_activated();

private:
	bool send();
	bool nextSend();
	quint64 totalTime() const;

public:
	int m_id;
	JICMPSocketPrivate *d_ptr;
	QSocketNotifier *m_notifier = nullptr;

	int m_socket = -1;
	struct sockaddr_in m_hostAddr;
	char m_pack[128] {0};
	long long m_sendTsUs;

	int m_timerID = -1;
	int m_totalCount = 0;
	int m_successCount = 0;
	int m_counter = 0;

	QList<quint64> m_TCR;
	LookupHost m_lookupHost;
};

static long long icmp_get_time_us();
static uint16_t inet_cksum(uint16_t *addr, int nleft);

PingTask::PingTask(int id, JICMPSocketPrivate *d_ptr) :
	QObject(d_ptr),
	m_id(id),
	d_ptr(d_ptr)
{
	m_lookupHost.setProperty("__PingTask", QVariant::fromValue(J_RCT(ABW_t, this)));
}

PingTask::~PingTask()
{
	if( m_socket >= 0 )
		::close(m_socket);
}

bool PingTask::start(const QHostAddress &address, int cnt)
{
	struct addrinfo hint;
	memset(&hint, 0, sizeof(struct addrinfo));

	hint.ai_family = AF_INET;
	hint.ai_socktype = SOCK_STREAM;

	QString qstrip = address.toString();
	if( qstrip.startsWith("ffff::") )
		qstrip.remove(0,6);

	if( QHostAddress().setAddress(qstrip) == false )
	{
		jFatal("Code bug.");
		return false;
	}
	auto ip = qstrip.toStdString();

	struct addrinfo *res;
	getaddrinfo(ip.c_str(), nullptr, &hint, &res);

	memcpy(&m_hostAddr, res->ai_addr, sizeof(struct sockaddr_in));
	freeaddrinfo(res);

	m_socket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
	if( m_socket < 0 )
	{
		d_ptr->m_errorString = strerror(errno);
		jError() << "ICMP socket create failed:" << d_ptr->m_errorString;
		return false;
	}

#if 0
	int opt = 1;
	setsockopt(m_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));
	opt = 1;
	setsockopt(m_socket, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(int));
	opt = 2000;
	setsockopt(m_socket, SOL_SOCKET, SO_RCVTIMEO, &opt, sizeof(int));
	opt = 2000;
	setsockopt(m_socket, SOL_SOCKET, SO_SNDTIMEO, &opt, sizeof(int));

	struct ifreq ifr;
	strcpy(ifr.ifr_name, "ens33");

	if( setsockopt(m_socket, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr)) != 0 )
	{
		d_ptr->m_errorString = strerror(errno);
		jError() << "bindto iface %s failed:" << d_ptr->m_errorString;
		return false;
	}
#endif

	m_notifier = new QSocketNotifier(m_socket, QSocketNotifier::Read, this);
	connect(m_notifier, &QSocketNotifier::activated, this, &PingTask::on_activated);

	m_totalCount = cnt;
	Q_ASSERT(m_timerID == -1);

	if( send() )
	{
		m_timerID = startTimer(d_ptr->m_timeout);
		return true;
	}
	return false;
}

void LookupHost::on_lookupHost(const QHostInfo &info)
{
	auto pingTask = J_RCT(PingTask*, property("__PingTask").value<ABW_t>());
	auto addrs = info.addresses();

	if( not addrs.isEmpty() )
		pingTask->start(addrs[0], pingTask->m_totalCount);
	else
	{
		pingTask->d_ptr->m_errorString = "Domain name resolution failure.";
		jDebug() << "Domain name resolution failure.";
		emit pingTask->d_ptr->q_ptr->error();
	}
}

inline bool PingTask::start(const QString &domainName, int cnt)
{
	m_totalCount = cnt;
	QHostInfo::lookupHost(domainName, &m_lookupHost, SLOT(on_lookupHost(QHostInfo)));
	return true;
}

inline QString PingTask::hostAddressString() const
{
	char hostIP[64] = "";
	if( inet_ntop(AF_INET, &m_hostAddr.sin_addr, hostIP, 64) == nullptr )
	{
		jError() << "inet_ntop error:" << strerror(errno);
		return QString();
	}
	return hostIP;
}

void PingTask::timerEvent(QTimerEvent *event)
{
	QObject::timerEvent(event);
	if( event->timerId() != m_timerID )
		return ;

	if( m_timerID != -1 )
	{
		killTimer(m_timerID);
		m_timerID = -1;
	}

	m_TCR << d_ptr->m_timeout;
	nextSend();
}

void PingTask::on_activated()
{
	if( m_timerID != -1 )
	{
		killTimer(m_timerID);
		m_timerID = -1;
	}

	char pack[128] = {0};
	struct sockaddr_in from;
	socklen_t fromlen = sizeof(from);

	int res = recvfrom(m_socket, pack, sizeof(pack), 0, (struct sockaddr*)&from, &fromlen);
	if( res <= 0 )
	{
		d_ptr->m_errorString = strerror(errno);
		jError() << "ICMP socket read failed:" << d_ptr->m_errorString;
		emit d_ptr->q_ptr->error();

		auto _icmphdr = J_RCT(struct icmphdr*, m_pack);
		if( _icmphdr->un.echo.sequence > 0 )
		{
			quint64 time = totalTime();
			JPingResponse response;

			response.taskID      = m_id;
			response.hostAddress = hostAddressString();
			response.averageTime = time / m_TCR.size();
			response.totalTime   = time;
			response.successCnt  = m_successCount;
			response.totalCnt    = m_totalCount;

			emit d_ptr->q_ptr->pong(response);
		}
		return deleteLater();
	}

	long long recv_ts_us = icmp_get_time_us();
	long long time_us = recv_ts_us - m_sendTsUs;

	struct iphdr *_iphdr = J_RCT(struct iphdr*, pack);
	auto _icmphdr = J_RCT(struct icmphdr*, pack + (_iphdr->ihl << 2));
	char *icmpdata = pack + (_iphdr->ihl << 2) + sizeof(struct icmphdr);

	long long sendTsUsFrom;
	memcpy(&sendTsUsFrom, icmpdata, sizeof(m_sendTsUs));

	if( _icmphdr->type == ICMP_ECHOREPLY and
		_icmphdr->un.echo.id == J_RCT(struct icmphdr*, m_pack)->un.echo.id and
		sendTsUsFrom == m_sendTsUs)
	{ // 接收到了正确的应答包,统计总时间和成功次数,结束当前接收
		m_TCR << time_us;
		++m_successCount;
		nextSend();
	}
}

bool PingTask::send()
{
	memset(m_pack, 0, sizeof(m_pack));
	auto _icmphdr = J_RCT(struct icmphdr*, m_pack);

	_icmphdr->type = ICMP_ECHO;
	_icmphdr->code = 0;
	_icmphdr->un.echo.id = getpid() & 0xFFFF;
	_icmphdr->un.echo.sequence = m_counter;

	m_sendTsUs = icmp_get_time_us();
	memcpy(m_pack + sizeof(struct icmphdr), &m_sendTsUs, sizeof(m_sendTsUs));
	J_RCT(struct icmphdr*, m_pack)->checksum = inet_cksum(J_RCT(quint16*, &m_pack), 64);

	if( sendto(m_socket, m_pack, 64, 0, (struct sockaddr*)&m_hostAddr, sizeof(struct sockaddr_in)) < 0 )
	{
		d_ptr->m_errorString = strerror(errno);
		jError() << "ICMP socket write failed:" << d_ptr->m_errorString;
		emit d_ptr->q_ptr->error();
		return false;
	}
	return true;
}

bool PingTask::nextSend()
{
	if( ++m_counter < m_totalCount and send() )
	{
		Q_ASSERT(m_timerID == -1);
		m_timerID = startTimer(d_ptr->m_timeout);
		return true;
	}

	quint64 time = totalTime();
	JPingResponse response;

	response.taskID      = m_id;
	response.hostAddress = hostAddressString();
	response.averageTime = time / m_TCR.size();
	response.totalTime   = time;
	response.successCnt  = m_successCount;
	response.totalCnt    = m_totalCount;

	emit d_ptr->q_ptr->pong(response);

	deleteLater();
	return false;
}

inline quint64 PingTask::totalTime() const
{
	quint64 sum = 0;
	for(auto &t : m_TCR)
		sum += t;
	return sum;
}

/*-------------------------------------------------------------------------------------------------*/

#define JICMPSocketUnix  JICMPSocketPlatform

class Q_DECL_HIDDEN JICMPSocketUnix : public QObject
{
public:
	explicit JICMPSocketUnix(JICMPSocketPrivate *d_ptr);

public:
	int newPingTask(const QHostAddress &address, int cnt);
	int newPingTask(const QString &domainName, int cnt);
	int waitForPong(JPingResponse &response, int timeout);

public:
	JICMPSocketPrivate *d_ptr;
	QHash<int, PingTask*> m_taskHash;
	int gID = 0;
};

#define __PING(host, hoststr) \
({ \
	int id = gID++; \
	auto task = new PingTask(id, d_ptr); \
	if( task->start(host, cnt) ) { \
		Q_ASSERT(m_taskHash.find(id) == m_taskHash.end()); \
		m_taskHash.insert(id, task); \
		connect(task, &PingTask::destroyed, this, [this,id](){ \
			m_taskHash.remove(id); \
		}); \
		return id; \
	} \
	delete task; \
	return -1; \
})

JICMPSocketUnix::JICMPSocketUnix(JICMPSocketPrivate *d_ptr) :
	QObject(d_ptr), d_ptr(d_ptr)
{

}

int JICMPSocketUnix::newPingTask(const QHostAddress &address, int cnt)
{
	QString qstrip = address.toString();
	if( qstrip.startsWith("ffff::") )
		qstrip.remove(0,6);
	__PING(address, qstrip);
}

int JICMPSocketUnix::newPingTask(const QString &domainName, int cnt)
{
	__PING(domainName, domainName);
}

int JICMPSocketUnix::waitForPong(JPingResponse&, int)
{
	jFatal("JICMPSocket::waitForPong: The functionality is not yet implemented. (unix/linux)");
	return -1;
}

/*-------------------------------------------------------------------------------------------------*/

JICMPSocketPrivate::JICMPSocketPrivate(JICMPSocket *q_ptr) :
	QObject(q_ptr),
	q_ptr(q_ptr),
	m_platform(new JICMPSocketUnix(this))
{

}

JICMPSocketPrivate::~JICMPSocketPrivate()
{

}

int JICMPSocketPrivate::ping(const QHostAddress &address, int cnt)
{
	return m_platform->newPingTask(address, cnt);
}

int JICMPSocketPrivate::ping(const QString &domainName, int cnt)
{
	return m_platform->newPingTask(domainName, cnt);
}

int JICMPSocketPrivate::waitForPong(JPingResponse &response, int timeout)
{
	return m_platform->waitForPong(response, timeout);
}

/*-------------------------------------------------------------------------------------------------*/

// 获取微秒时间戳
static long long icmp_get_time_us()
{
	struct timespec tm;
	clock_gettime(CLOCK_MONOTONIC, &tm);
	long long ret = tm.tv_sec;
	ret = ret * 1000000 + tm.tv_nsec / 1000;
	return ret;
}

// 此函数来源于busybox
static uint16_t inet_cksum(uint16_t *addr, int nleft)
{
# define BB_LITTLE_ENDIAN 1
	/*
   * Our algorithm is simple, using a 32 bit accumulator,
   * we add sequential 16 bit words to it, and at the end, fold
   * back all the carry bits from the top 16 bits into the lower
   * 16 bits.
   */
	unsigned sum = 0;
	while (nleft > 1) {
		sum += *addr++;
		nleft -= 2;
	}

	/* Mop up an odd byte, if necessary */
	if (nleft == 1) {
		if (BB_LITTLE_ENDIAN)
			sum += *(uint8_t*)addr;
		else
			sum += *(uint8_t*)addr << 8;
	}

	/* Add back carry outs from top 16 bits to low 16 bits */
	sum = (sum >> 16) + (sum & 0xffff);     /* add hi 16 to low 16 */
	sum += (sum >> 16);                     /* add carry */

	return (uint16_t)~sum;
}

} //namespace QTE

#endif //unix/linux
